﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using CuteAnt.Reflection;
using Grace.Utilities;

namespace Grace.DependencyInjection.Impl.Wrappers
{
  /// <summary>Strategy to create delegate that has one arguement</summary>
  public class DelegateOneArgWrapperStrategy : BaseWrapperStrategy
  {
    /// <summary>Default constructor</summary>
    /// <param name="activationType"></param>
    /// <param name="injectionScope"></param>
    public DelegateOneArgWrapperStrategy(Type activationType, IInjectionScope injectionScope)
      : base(activationType, injectionScope) { }

    /// <summary>Get the type that is being wrapped</summary>
    /// <param name="type">requested type</param>
    /// <returns>wrapped type</returns>
    public override Type GetWrappedType(Type type)
    {
      var invokeMethod = type.GetTypeInfo().GetDeclaredMethod(InvokeMethodName);

      return invokeMethod?.ReturnType;
    }

    /// <summary>Get an activation expression for this strategy</summary>
    /// <param name="scope"></param>
    /// <param name="request"></param>
    /// <returns></returns>
    public override IActivationExpressionResult GetActivationExpression(IInjectionScope scope, IActivationExpressionRequest request)
    {
      var activationType = request.ActivationType;
      var invokeMethod = activationType.GetTypeInfo().GetDeclaredMethod(InvokeMethodName);

      var list = new List<Type>(invokeMethod.GetParameters().Select(p => p.ParameterType))
      {
        invokeMethod.ReturnType,
        activationType
      };

      // ## 苦竹 修改 ##
      //var closedClass = typeof(DelegateExpression<,,>).MakeGenericType(list.ToArray());
      var closedClass = typeof(DelegateExpression<,,>).GetCachedGenericType(list.ToArray());

      var closedMethod = closedClass.GetRuntimeMethod(CreateDelegateMethodName, 
          new[] { typeof(IExportLocatorScope), typeof(IDisposalScope), typeof(IInjectionContext) });

      // ## 苦竹 修改 ##
      //var instance = Activator.CreateInstance(closedClass, scope, request, request.Services.InjectionContextCreator, this);
      var instance = ActivatorUtils.CreateInstance(closedClass, scope, request, request.Services.InjectionContextCreator, this);

      var callExpression =
          Expression.Call(Expression.Constant(instance), closedMethod, request.ScopeParameter,
              request.DisposalScopeExpression, request.InjectionContextParameter);

      return request.Services.Compiler.CreateNewResult(request, callExpression);
    }

    /// <summary>Helper class to create delegate expression</summary>
    /// <typeparam name="TArg1"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <typeparam name="TDelegate"></typeparam>
    public class DelegateExpression<TArg1, TResult, TDelegate>
    {
      private readonly IInjectionContextCreator _injectionContextCreator;
      private readonly string _arg1Id = UniqueStringId.Generate();
      private readonly ActivationStrategyDelegate _action;
      private readonly MethodInfo _funcMethodInfo;

      /// <summary>Default constructor</summary>
      /// <param name="scope"></param>
      /// <param name="request"></param>
      /// <param name="injectionContextCreator"></param>
      /// <param name="activationStrategy"></param>
      public DelegateExpression(IInjectionScope scope, IActivationExpressionRequest request,
        IInjectionContextCreator injectionContextCreator, IActivationStrategy activationStrategy)
      {
        _injectionContextCreator = injectionContextCreator;

        var newRequest = request.NewRequest(typeof(TResult), activationStrategy, typeof(TDelegate), RequestType.Other, null, true);

        newRequest.AddKnownValueExpression(CreateKnownValueExpression(request, typeof(TArg1), _arg1Id));

        newRequest.SetLocateKey(request.LocateKey);
        newRequest.DisposalScopeExpression = request.Constants.RootDisposalScope;

        var activationExpression = request.Services.ExpressionBuilder.GetActivationExpression(scope, newRequest);

        _action = request.Services.Compiler.CompileDelegate(scope, activationExpression);

        _funcMethodInfo = typeof(FuncClass).GetTypeInfo().GetDeclaredMethod(FuncClass.FuncMethodName);
      }

      /// <summary>MEthod to create delegate</summary>
      /// <param name="scope"></param>
      /// <param name="disposalScope"></param>
      /// <param name="context"></param>
      /// <returns></returns>
      public TDelegate CreateDelegate(IExportLocatorScope scope, IDisposalScope disposalScope,
          IInjectionContext context)
      {
        var funcClass = new FuncClass(scope, disposalScope, context, _action, _injectionContextCreator, _arg1Id);

        return (TDelegate)((object)_funcMethodInfo.CreateDelegate(typeof(TDelegate), funcClass));
      }

      /// <summary>Helper class that has method that is turned into delegate</summary>
      public class FuncClass
      {
        private readonly IExportLocatorScope _scope;
        private readonly IDisposalScope _disposalScope;
        private readonly string _arg1Id;
        private readonly IInjectionContext _context;
        private readonly ActivationStrategyDelegate _action;
        private readonly IInjectionContextCreator _injectionContextCreator;

        /// <summary>Default constructor</summary>
        /// <param name="scope"></param>
        /// <param name="disposalScope"></param>
        /// <param name="context"></param>
        /// <param name="action"></param>
        /// <param name="injectionContextCreator"></param>
        /// <param name="arg1Id"></param>
        public FuncClass(IExportLocatorScope scope, IDisposalScope disposalScope, IInjectionContext context, 
          ActivationStrategyDelegate action, IInjectionContextCreator injectionContextCreator, string arg1Id)
        {
          _scope = scope;
          _disposalScope = disposalScope;
          _context = context;
          _action = action;
          _injectionContextCreator = injectionContextCreator;
          _arg1Id = arg1Id;
        }

        internal const string FuncMethodName = nameof(Func);
        /// <summary>MEthod that is turned into delegate</summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        public TResult Func(TArg1 arg)
        {
          var newContext = _context?.Clone() ?? _injectionContextCreator.CreateContext(null);

          newContext.SetExtraData(_arg1Id, arg);

          return (TResult)_action(_scope, _disposalScope, newContext);
        }
      }
    }
  }
}